package com.ctrip.corp.obt.shard.sql.mybatis.interceptor.inner;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.stream.Collectors;

import com.ctrip.corp.obt.generic.utils.Md5Utils;
import com.ctrip.corp.obt.generic.utils.StringPool;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;

import com.ctrip.corp.obt.generic.core.context.TenantContext;
import com.ctrip.corp.obt.generic.utils.EnvironmentHolder;
import com.ctrip.corp.obt.generic.utils.ReflectionUtils;
import com.ctrip.corp.obt.generic.utils.StringUtils;
import com.ctrip.corp.obt.metric.Metrics;
import com.ctrip.corp.obt.metric.log.model.MetricMeta;
import com.ctrip.corp.obt.shard.holder.ShardSourceHolder;
import com.ctrip.corp.obt.shard.sql.parser.AbstractSqlParserSupport;
import com.ctrip.corp.obt.shard.utils.PluginUtils;

import lombok.EqualsAndHashCode;
import lombok.ToString;
import tk.mybatis.mapper.entity.Example;

/**
 * 用来收集服务中使用过的sql语句，并记录
 *
 * @author x.shen
 * @since 2024/3/5
 */
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class SqlMockPlugin extends AbstractSqlParserSupport implements InnerPlugin {

    @Override
    public int order() {
        return 1;
    }

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        String tenantId = TenantContext.getTenantId();
        if (StringUtils.isBlank(tenantId) || !Optional.ofNullable(ShardSourceHolder.getSqlPluginsCache().get(tenantId))
            .map(pluginName -> pluginName.contains(SqlMockPlugin.class.getSimpleName()))
            .orElse(false)) {
            return;
        }

        try {
            PluginUtils.DSStatementHandler dsSh = PluginUtils.dsStatementHandler(sh);
            MappedStatement ms = dsSh.mappedStatement();
            PluginUtils.DSBoundSql dsBoundSql = dsSh.dsBoundSql();

            // INSERT INTO mb_org_info(id,NAME,org_type,parent_id,org_id,LEVEL,valid,delete_time) value
            // (?,?,?,?,?,?,?,?)
            String originalSqlMock = dsBoundSql.sql();
            String sql = originalSqlMock;
            // sql类型
            SqlCommandType sqlCommandType = ms.getSqlCommandType();

            List<ParameterMapping> parameterMappings = dsBoundSql.parameterMappings();
            // 获取赋值的字段名
            List<String> propertyNameList =
                parameterMappings.stream().map(ParameterMapping::getProperty).collect(Collectors.toList());
            Map<String, Object> parameterValue = dsBoundSql.additionalParameters();

            // 动态sql的value收集
            List<String> dynamicValueList = new ArrayList<>();

            // 目前入参可能为对象、list、以及单个参数等
            for (String name : propertyNameList) {
                // 先处理对象入参
                Field field = ReflectionUtils.findField(dsBoundSql.parameterObject().getClass(), name);
                if (Objects.nonNull(field)) {
                    ReflectionUtils.makeAccessible(field);
                    Object fieldObject = field.get(dsBoundSql.parameterObject());
                    sql = sql.replaceFirst("\\?", convertString(fieldObject));
                    dynamicValueList.add(String.valueOf(fieldObject));
                    continue;
                }

                // 处理列表
                if (parameterValue.containsKey(name)) {
                    Object value = parameterValue.get(name);
                    sql = sql.replaceFirst("\\?", convertString(value));
                    dynamicValueList.add(String.valueOf(value));
                    continue;
                }
                
                // 处理 example 对象
                if (name.contains(".value")) {
                    String exampleKey = name.substring(0, name.indexOf("."));
                    if(parameterValue.containsKey(exampleKey)){
                        Object exampleValue = parameterValue.get(exampleKey);
                        if (exampleValue instanceof Example.Criterion) {
                            Example.Criterion example = (Example.Criterion)exampleValue;
                            Object value = example.getValue();
                            sql = sql.replaceFirst("\\?", convertString(value));
                            dynamicValueList.add(String.valueOf(value));
                        }
                        continue;
                    }
                }

                // 处理单一参数
                Object parameteredObject = dsBoundSql.parameterObject();
                if (parameteredObject instanceof Map) {
                    Map<String, Object> parameteredObjectMap = (Map<String, Object>)parameteredObject;
                    Object value = parameteredObjectMap.get(name);
                    sql = sql.replaceFirst("\\?", convertString(value));
                    dynamicValueList.add(String.valueOf(value));
                }
            }
            


            // 这里处于同一个会话（连接串中），如果 z_sql_mock 表中有主键id，那么在外部使用insert语句并需要回写主键时，这里的insert就会影响主键值
            // 步骤就是先执行外部的insert，之后执行 SELECT LAST_INSERT_ID() ，在执行 z_sql_mock 的 insert，再执行SELECT LAST_INSERT_ID()，
            // 最终将 z_sql_mock 的 insert 的主键值覆盖外部的 insert 的主键值。这是非常严重的事情!!!!
            // 解决办法：1.去掉 z_sql_mock 表中的主键；2.创建新的会话连接，去执行该sql
            String insertSql = "INSERT INTO z_sql_mock (sql_mock_type, original_sql_md5, original_sql_mock, dynamic_value, sql_mock, application) VALUES (?,?,?,?,?,?)";

            String md5Hex = Md5Utils.md5Hex(originalSqlMock);
            String dynamicValues = String.join("♀", dynamicValueList);

            // 后续ck全部铺设后，需要将其下掉
            try (PreparedStatement preparedStatement = connection.prepareStatement(insertSql)) {
                preparedStatement.setString(1, sqlCommandType.toString());
                preparedStatement.setString(2, md5Hex);
                preparedStatement.setString(3, originalSqlMock); // No need for manual escaping; PreparedStatement handles it
                preparedStatement.setString(4, dynamicValues); // No need for manual escaping; PreparedStatement handles it
                preparedStatement.setString(5, sql); // No need for manual escaping; PreparedStatement handles it
                preparedStatement.setString(6, EnvironmentHolder.getProperty("spring.application.name"));
                preparedStatement.execute();
            } catch (SQLException e) {
                // 为了不影响平时排障操作，这里发生异常时捕获，同时，使用随机数，当命中时，才打印出故障日志
                Random random = new Random(0);
                int index = random.nextInt(10) + 1;
                if(index == 3){
                    log.error("===> 这里是sqlMock收集异常，不是业务异常，排障时请忽略！sqlMock异常为：{}",e);
                }
            }

            // ck收集，目前没有使用，改用落数据库的方式收集
            Metrics.LOGGER.collect(
                    new MetricMeta("collectSqlMock")
                            .withTag("sql_mock_type", sqlCommandType.toString())
                            .withTag("original_sql_md5", md5Hex)
                            .withTag("original_sql_mock", originalSqlMock)
                            .withTag("dynamic_value", dynamicValues)
                            .withTag("sql_mock", sql)
                            .withTag("application", EnvironmentHolder.getProperty("spring.application.name")));
        } catch (Exception e) {
            // 为了不影响平时排障操作，这里发生异常时捕获，同时，使用随机数，当命中时，才打印出故障日志
            Random random = new Random(0);
            int index = random.nextInt(10) + 1;
            if(index == 3){
                log.error("===> 这里是sqlMock收集异常，不是业务异常，排障时请忽略！sqlMock异常为：{}",e);
            }
        }
    }

    private String convertString(Object value){
        return "'" + value + "'";
    }
}
